﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ListIt.DAL;
using Dispatcher.Presenter;
using System.Security.Cryptography.X509Certificates;
using System.Data.Objects;
using SilverlightShared;
using C2dmSharp.Server;
using System.Windows.Forms;
using System.Collections.Specialized;
using System.Net.Sockets;
using System.Net;
using System.Threading;

namespace Dispatcher
{
    public interface IBackLogView
    {
        object BackLogDataSource { set; }
        object SummaryDataSource { set; }
        void Log(string s);
        void SendJobTimeoutToDriver(string licenseNo);
    }

    public class JobSummary
    {
        public string Queue { get; set; }
        public int Count { get; set; }
    }
    
    public class JobBackLogModelView
    {
        public int ID { get; set; }
        public bool WaitingForReply { get; set; }
        public DateTime TimeRequested { get; set; }
        public string Customer { get; set; }
        public string Pickup { get; set; }
        public string Mobile { get; set; }
        public string DeclinedDrivers { get; set; }
        public string VehicleRequested { get; set; }
        public DateTime? DispatchedTime { get; set; }
    }

    public class BacklogPresenter : GenericPresenter
    {
        #region Constants       
        const int C_PING_RADIUS_A = 5;
        const int C_PING_RADIUS_B = 10;
        const int C_PING_RADIUS_C = 15;
        const int C_PING_RADIUS_D = 20;
        #endregion

        #region Private Vars
        private IBackLogView _View;
        //private X509Certificate2 _clientCertificate;
        private C2dmService _service;
        #endregion

        #region Constructor & Destructor
        public BacklogPresenter(IBackLogView view, C2dmService service) : base()
        {
            _View = view;
            _service = service;
        }
        #endregion

        #region Private Fn
        private bool DispatchJobUsingSocket(JobBackLog job)
        {
            Taxi taxi = DAL.ListItEntities.Taxis.SingleOrDefault(t => t.JobPending == job.ID);

            // if job doesn't have a taxi then find a suiteable one
            if (taxi == null) 
                taxi = FindSuitableTaxi(Convert.ToInt16(job.VehicleType), job.DeclinedDrivers, Convert.ToDouble(job.PickupLongituted), 
                    Convert.ToDouble(job.PickupLatitude));
               
            if ((taxi != null)  && (taxi.IPAddress != null))
            {
                /* this keeps track on how many times the auto dispatch occured for this job.
                 * Do not put this in the Dispatch(JOB, TAXI) Fn because that is also called from manual dispatch */
                job.DriverAssignmentMarker = DateTime.Now;
                job.DispatchCount += 1; 

                return DispatchJob(job, taxi);
            }
            else
            {
                return false;  // no suitable taxi
            }
        }

        private bool DispatchJob(JobBackLog job)
        {
            if (job.PendingDriver) // re-dispatch
            {

                var collection = from taxi in DAL.ListItEntities.Taxis
                                 where (taxi.JobPending == job.ID)
                                 select taxi;

                List<Taxi> taxiList = collection.ToList();

                foreach (Taxi taxi in taxiList)
                {

                    if (taxi != null)
                    {
                        _View.Log(string.Format("Sending secondary dispatch to {0}", taxi.LicenseNo));

                        var extras = new NameValueCollection();

                        //Queue up the message to be sent
                        _service.QueueMessage(taxi.DeviceToken, extras, ProtocolConstants.C2DM_COLLAPSE_KEY_JOB);
                        job.DispatchDate = DateTime.Now;
                        job.DispatchCount += 1;
                        DAL.ListItEntities.SaveChanges();
                    } 
                }
            }
         
            return true;
        }

        private byte[] StringToBytes(string strInput)
        {
            // Assume string is in this format "######## ######## ######## ######## ######## ######## ######## ########"
            System.Diagnostics.Debug.Assert(((strInput.Length + 1) % 9) == 0);

            int groupCount = (strInput.Length + 1) / 9;
            int byteCount = groupCount * 4;

            byte[] bytes = new byte[byteCount];
            for (int i = 0; i < byteCount; i++)
            {
                int offset = i * 2 + (i / 4);
                bytes[i] = Convert.ToByte(strInput.Substring(offset, 2), 16);
            }
            return bytes;
        }

        private Taxi FindSuitableTaxi(short vehicleType, string declinedDrivers, double pickupLong, double pickupLat)
        {
            var collection = from taxis in DAL.ListItEntities.Taxis
                             where (taxis.Available == true) && (taxis.JobPending == -1)
                             orderby taxis.TimeStatusChange
                             select taxis;

            List<Taxi> taxiList = new List<Taxi>();
            TaxiType taxiType = (TaxiType)vehicleType;
            foreach (Taxi taxi in collection)
            {
                switch (taxiType)
                {
                    case TaxiType.FourSeatSedan:
                        if ((taxi.VehicleType >= (short)TaxiType.FourSeatSedan) && (taxi.VehicleType <= (short)TaxiType.TenSeater))
                            taxiList.Add(taxi);
                        break;
                    case TaxiType.Stationwagon:
                        if (taxi.VehicleType == (short)TaxiType.Stationwagon)
                            taxiList.Add(taxi);
                        break;
                    case TaxiType.SevenSeater:
                        if ((taxi.VehicleType == (short)TaxiType.SevenSeater) || (taxi.VehicleType == (short)TaxiType.TenSeater))
                            taxiList.Add(taxi);
                        break;
                    case TaxiType.TenSeater:
                        if (taxi.VehicleType == (short)TaxiType.TenSeater)
                            taxiList.Add(taxi);
                        break;
                    case TaxiType.Executive:
                        if (taxi.VehicleType == (short)TaxiType.Executive)
                            taxiList.Add(taxi);
                        break;
                    case TaxiType.SpecialNeeds:
                        if (taxi.VehicleType == (short)TaxiType.SpecialNeeds)
                            taxiList.Add(taxi);
                        break;
                    case TaxiType.Limousine:
                        if (taxi.VehicleType == (short)TaxiType.Limousine)
                            taxiList.Add(taxi);
                        break;
                    default:
                        break;
                }
            }

            double taxiDistance;

            List<string> declinedDriverList;
            if (declinedDrivers != null)
                declinedDriverList = declinedDrivers.Split(',').ToList();
            else
                declinedDriverList = new List<string>();

            Taxi closestTaxi = null;
            double currentDistance = C_PING_RADIUS_A;

            // first pass - within 5km radius
            foreach (Taxi taxi in taxiList)
            {
                // now we need to look for a suitable taxi - closest and available                       
                if (declinedDriverList.IndexOf(taxi.LicenseNo.Trim()) == -1)
                {
                    // this driver hasn't declined the job yet                                        
                    taxiDistance = ListIt.DAL.GeoStaicClass.distance(Convert.ToDouble(taxi.Latitude), Convert.ToDouble(taxi.Longituted),
                        pickupLat, pickupLong, 'K');

                    if (taxiDistance <= currentDistance)
                    {
                        currentDistance = taxiDistance;
                        closestTaxi = taxi;
                    }
                }
            }

            if (closestTaxi != null)
                return closestTaxi;

            currentDistance = C_PING_RADIUS_B;
            // second pass - within 10km radius
            foreach (Taxi taxi in taxiList)
            {
                // now we need to look for a suitable taxi - closest and available                       
                if (declinedDriverList.IndexOf(taxi.LicenseNo.Trim()) == -1)
                {
                    // this driver hasn't declined the job yet                                        
                    taxiDistance = ListIt.DAL.GeoStaicClass.distance(Convert.ToDouble(taxi.Latitude), Convert.ToDouble(taxi.Longituted),
                        pickupLat, pickupLong, 'K');

                    if (taxiDistance <= currentDistance)
                    {
                        currentDistance = taxiDistance;
                        closestTaxi = taxi;
                    }
                }
            }

            if (closestTaxi != null)
                return closestTaxi;

            // third pass - within 15km radius
            currentDistance = C_PING_RADIUS_C;
            foreach (Taxi taxi in taxiList)
            {
                // now we need to look for a suitable taxi - closest and available                       
                if (declinedDriverList.IndexOf(taxi.LicenseNo.Trim()) == -1)
                {
                    // this driver hasn't declined the job yet                                        
                    taxiDistance = ListIt.DAL.GeoStaicClass.distance(Convert.ToDouble(taxi.Latitude), Convert.ToDouble(taxi.Longituted),
                        pickupLat, pickupLong, 'K');

                    if (taxiDistance <= currentDistance)
                    {
                        currentDistance = taxiDistance;
                        closestTaxi = taxi;
                    }
                }
            }

            if (closestTaxi != null)
                return closestTaxi;

            // forth pass - within 20km radius
            currentDistance = C_PING_RADIUS_D;
            foreach (Taxi taxi in taxiList)
            {
                // now we need to look for a suitable taxi - closest and available                       
                if (declinedDriverList.IndexOf(taxi.LicenseNo.Trim()) == -1)
                {
                    // this driver hasn't declined the job yet                                        
                    taxiDistance = ListIt.DAL.GeoStaicClass.distance(Convert.ToDouble(taxi.Latitude), Convert.ToDouble(taxi.Longituted),
                        pickupLat, pickupLong, 'K');

                    if (taxiDistance <= currentDistance)
                    {
                        currentDistance = taxiDistance;
                        closestTaxi = taxi;
                    }
                }
            }

            return closestTaxi;
        }
        #endregion

        #region Public Fn
        public bool DispatchJob(JobBackLog job, Taxi taxi)
        {
            _View.Log(string.Format("Dispatching job ({0}) to: {1} - {2}", job.ID, taxi.LicenseNo, taxi.IPAddress));

            try
            {
                /* We have to mark the job as taken first before doing the actual dispatch
                 * because connecting to device might take time especially if the 
                 * device's IP is not reachable. */

                job.DispatchDate = DateTime.Now;
                job.PendingDriver = true;
                taxi.JobPending = job.ID;
                DAL.ListItEntities.SaveChanges();

                // send ping to phone
                IPAddress ipAddress = IPAddress.Parse(taxi.IPAddress);
                IPEndPoint endPoint = new IPEndPoint(ipAddress, 53709);
                TcpClient tcpClient = new TcpClient();
                try
                {
                    tcpClient.ExclusiveAddressUse = true;
                    tcpClient.Connect(endPoint);
                }
                finally
                {
                    tcpClient.Client.Shutdown(SocketShutdown.Both);
                    tcpClient.Close();
                }
                return true;
            }
            catch (Exception e)
            {
                _View.Log(e.Message);
                return false;
            }
        }

        public bool DispatchJobs()
        {
            bool result = false; // no job to dispatch

            DAL.ListItEntities.Refresh(RefreshMode.ClientWins, DAL.ListItEntities.JobBackLogs);
            DAL.ListItEntities.Refresh(RefreshMode.ClientWins, DAL.ListItEntities.Taxis);
            
            // dispatch jobs that doesn't have a pending driver
            var jobs = from jobBacklog in DAL.ListItEntities.JobBackLogs
                       where (jobBacklog.PendingDriver == false)
                       select jobBacklog;

            result = jobs.Count() > 0;  // return true if we have job to dispatch

            List<JobBackLog> jobList = jobs.ToList();

            foreach (JobBackLog job in jobList)
            {
                DispatchJobUsingSocket(job);
            }

            #region check jobs that has been dispatched but driver has not replied inside 2 minutes
            jobs = from jobBacklog in DAL.ListItEntities.JobBackLogs
                       where (jobBacklog.PendingDriver == true)
                       select jobBacklog;

            jobList = jobs.ToList();

            foreach (JobBackLog job in jobList)
            {
                if (job.DriverAssignmentMarker == null)
                {
                    job.DriverAssignmentMarker = DateTime.Now;
                    DAL.ListItEntities.SaveChanges();
                }


                DateTime dt = Convert.ToDateTime(job.DriverAssignmentMarker);
                TimeSpan ts = DateTime.Now.Subtract(dt);

                if (ts.TotalMinutes > 1.5)
                {
                    Taxi taxi = DAL.ListItEntities.Taxis.SingleOrDefault(t => t.JobPending == job.ID);

                    if (taxi != null)
                    {
                        taxi.JobPending = -1;

                        // assign this taxi to the declined list on the current job
                        List<string> declinedDrivers;
                        if (job.DeclinedDrivers != null)
                            declinedDrivers = job.DeclinedDrivers.Split(',').ToList();
                        else
                            declinedDrivers = new List<string>();

                        declinedDrivers.Add(taxi.LicenseNo.Trim());
                        job.DeclinedDrivers = string.Join(",", declinedDrivers);
                        job.DriverAssignmentMarker = DateTime.Now;

                        // send message to driver
                        _View.SendJobTimeoutToDriver(taxi.LicenseNo);
                    }

                    job.PendingDriver = false;
                    job.DispatchDate = null;
                    job.DispatchCount = 0;
                    DAL.ListItEntities.SaveChanges();

                    // dispatch the job to someone else
                    DispatchJobs();
                }
            }
            #endregion

            
            #region check jobs that has been dispatched but driver has not replied inside 30 seconds

            // dispatch job again using socket method
            jobs = from jobBacklog in DAL.ListItEntities.JobBackLogs
                   where (jobBacklog.PendingDriver == true)
                   select jobBacklog;

            jobList = jobs.ToList();

            foreach (JobBackLog job in jobList)
            {
                if (job.DispatchDate != null)
                {
                    DateTime dt = Convert.ToDateTime(job.DispatchDate);
                    TimeSpan ts = DateTime.Now.Subtract(dt);

                    // redespatch in case
                    if (ts.TotalSeconds > 30)
                    {
                        DispatchJob(job);
                    }
                }
            }
            #endregion

            return result;
        }

        public override void Present()
        {
            // present backlog
            var backLogJobs = from jobs in DAL.ListItEntities.JobBackLogs
                          select jobs;
            List<JobBackLogModelView> backlogList = new List<JobBackLogModelView>();

            foreach (var job in backLogJobs)
            {
                TaxiType taxiType = (TaxiType)job.VehicleType;
                backlogList.Add (
                          new JobBackLogModelView
                          {
                              ID = job.ID,
                              WaitingForReply = job.PendingDriver,
                              TimeRequested = job.TravelDate,
                              Customer = job.CustomerName,
                              Pickup = job.PickupAddress,
                              Mobile = job.MobileNo,
                              DeclinedDrivers = job.DeclinedDrivers,
                              VehicleRequested = TaxiCommon.GetVehicleType(taxiType),
                              DispatchedTime = job.DispatchDate
                          });
            }

            _View.BackLogDataSource = backlogList;

            // present summary
            List<JobSummary> summaryList = new List<JobSummary>();

            var availableTaxis = from taxi in DAL.ListItEntities.Taxis
                                 where (taxi.Available == true)
                                 select taxi;
            var activeJobs = from activeJob in DAL.ListItEntities.ActiveJobs
                             select activeJob; 

            summaryList.Add(
                new JobSummary { 
                    Queue = "Available Taxi", 
                    Count = availableTaxis.Count()
                });

            summaryList.Add(
                new JobSummary
                {
                    Queue = "Job Waiting for dispatch",
                    Count = backlogList.Count()
                });

            summaryList.Add(
                 new JobSummary
                 {
                     Queue = "Active Jobs",
                     Count = activeJobs.Count()
                 });

            _View.SummaryDataSource = summaryList;
        }

        public void Delete(JobBackLogModelView job)
        {
            JobBackLog jobBacklog = DAL.ListItEntities.JobBackLogs.SingleOrDefault(j => j.ID == job.ID);

            if (jobBacklog != null)
            {
                DAL.ListItEntities.DeleteObject(jobBacklog);
                DAL.ListItEntities.SaveChanges();
            }
        }

        public void TearDown()
        {
           
        }
        #endregion
    }
}
